作り始めたBoltアプリケーションをAWSサーバーレス環境にデプロイ可能にするためにCDKを後から適用してみた
CX事業本部の阿部です。
先日から少しずつSlack Appを作っています。 バックエンドはBoltフレームワークを使って作っているのですが、テストをしている間はngrokを使ってローカルホストで動かしているサーバーにアクセスさせていました。
初期段階での利用に目処がたったので、AWSにデプロイ、できればLambdaを使ってサーバーレスな環境で運用したくなりました。 なおかつ、インフラ周りの構成はできればCDKに寄せたい。
ということで、一旦デプロイ先を考えずに素で作り始めたBoltアプリケーションのインフラ構成をするために後からCDKを適用してみました。
今回のゴール
- インフラ構成用とランタイム用の
package.json
を切り離す - ランタイム用の依存ライブラリとソースをデプロイする
実際にBoltアプリケーションのバックエンドをAWS上でどう構成するかはアーキテクチャの問題なので、まずは構成とアプリケーションを切り離してCDKを使ってデプロイ可能な状態を目指します。
前提
現在のアプリケーションはよくあるGetting Startedから拡張して作っています。
そのため、プロジェクトのディレクトリのトップにある package.json
やその他設定ファイルなどはBoltアプリケーションのための構成となっています。
アプリケーションのコードは src
配下に集約してあります。
$ ls -lart total 200 -rw-r--r-- 1 abe.shinsuke staff 857 3 3 17:54 .eslintrc -rw-r--r-- 1 abe.shinsuke staff 198 3 3 17:58 .eslintignore -rw-r--r-- 1 abe.shinsuke staff 124 3 11 10:05 .env -rw-r--r-- 1 abe.shinsuke staff 514 4 3 09:44 tsconfig.json drwxr-xr-x 8 abe.shinsuke staff 256 4 23 11:08 .. -rw-r--r-- 1 abe.shinsuke staff 25 4 24 09:47 .gitignore -rw-r--r-- 1 abe.shinsuke staff 73832 4 24 09:47 package-lock.json drwxr-xr-x 15 abe.shinsuke staff 480 4 24 09:47 . -rw-r--r-- 1 abe.shinsuke staff 790 4 24 09:47 package.json drwxr-xr-x 7 abe.shinsuke staff 224 4 24 09:47 src drwxr-xr-x 13 abe.shinsuke staff 416 4 24 09:47 .git
package.json
の内容は以下です。
{ "name": "cx-job-offer-meeting-tools", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "npm run build && npm run server", "build": "npx tsc", "server": "node --require dotenv/config dist/app.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@slack/bolt": "^1.6.0", "dotenv": "^8.2.0" }, "devDependencies": { "@types/eslint": "^6.1.8", "@types/node": "^13.7.7", "@typescript-eslint/eslint-plugin": "^2.22.0", "@typescript-eslint/parser": "^2.22.0", "eslint": "^6.8.0", "eslint-config-prettier": "^6.10.0", "eslint-plugin-prettier": "^3.1.2", "prettier": "^1.19.1", "typescript": "^3.8.3" } }
課題になりそうなところ(事前の検討)
前提を踏まえた上で、CDK適用の課題になりそうなところをリストアップしてみます。
cdk init
とかそのままやって大丈夫か?- Boltアプリケーションの起動の仕組みがLambdaと共存するか?
- CDKのインフラ構成のためのコードと設定の適用
下調べ
実際のプランを検討する前に、情報を集めて下調べをしておきましょう。
BoltのアプリケーションをLambdaで呼ぶためのコードについて確認してみる
Serverless Frameworkを使って、Boltアプリケーションをクラウドにデプロイする際のサンプルがSlackの瀬良さんの資料の中で公開されています。
【レポート】Bolt を使った Slack 連携アプリの開発からデプロイ・運用まで超入門 – Developers.IO TOKYO 2019 #cmdevio
当該部分を抜粋します。
const expressReceiver = new ExpressReceiver ({ signingSecret: サイニングシークレット, endpoints: '/events' }); const app = new App({ receiver: expressReceiver, token: トークン }) const awsServerlessExpress = require('aws-serverless-express'); const server = awsServerlessExpress.createServer(expressReceiver.app); module.exports.app = (event, context) => { awsServerlessExpress.proxy(server, event, context); }
BoltアプリケーションはExpressで動いているので、 aws-serverless-express でハンドラ関数からプロキシすれば良いようです。
cdk init
で作成されるリソースを確認してみる
次に、CDKでサービスを構成するために必要な物を整理しましょう。
cdk init
で作成されるファイル、プロジェクト構成は以下のようになっています。
$ ls -lart total 504 -rw-r--r-- 1 abe.shinsuke staff 135 4 22 10:18 .gitignore -rw-r--r-- 1 abe.shinsuke staff 65 4 22 10:18 .npmignore -rw-r--r-- 1 abe.shinsuke staff 543 4 22 10:18 README.md drwxr-xr-x 3 abe.shinsuke staff 96 4 22 10:18 bin -rw-r--r-- 1 abe.shinsuke staff 130 4 22 10:18 jest.config.js drwxr-xr-x 3 abe.shinsuke staff 96 4 22 10:18 lib -rw-r--r-- 1 abe.shinsuke staff 536 4 22 10:18 package.json drwxr-xr-x 3 abe.shinsuke staff 96 4 22 10:18 test -rw-r--r-- 1 abe.shinsuke staff 596 4 22 10:18 tsconfig.json -rw-r--r-- 1 abe.shinsuke staff 157 4 22 10:18 cdk.json drwxr-xr-x 12 abe.shinsuke staff 384 4 22 10:18 .git drwxr-xr-x 467 abe.shinsuke staff 14944 4 22 10:18 node_modules -rw-r--r-- 1 abe.shinsuke staff 227332 4 22 10:18 package-lock.json drwxr-xr-x 15 abe.shinsuke staff 480 4 22 10:18 . drwxr-xr-x 8 abe.shinsuke staff 256 4 23 11:08 ..
この中で注目すべきは、 package.json
の記載内容と bin
配下のファイルと lib
配下のファイルです。
package.json
を見ていきます。
{ "name": "cdk-test", "version": "0.1.0", "bin": { "cdk-test": "bin/cdk-test.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk" }, "devDependencies": { "@aws-cdk/assert": "1.34.0", "@types/jest": "^25.2.1", "@types/node": "10.17.5", "jest": "^25.3.0", "ts-jest": "^25.3.1", "aws-cdk": "1.34.0", "ts-node": "^8.1.0", "typescript": "~3.7.2" }, "dependencies": { "@aws-cdk/core": "1.34.0", "source-map-support": "^0.5.16" } }
CDKのプロジェクトなので、インフラ構成をするためのコードを実行するための依存関係になっています。
CDKのプロジェクトで実行時に依存関係を持ったライブラリがあるLambdaをデプロイする場合、そのままnode_modulesを一緒にデプロイしてしまうとノイズが多いので、ランタイム用の package.json
を切り離す必要があります。
次に bin
ディレクトリの配下ですが、CDKのConstructを実行するコードが置かれています。
$ ls -lart bin total 8 drwxr-xr-x 3 abe.shinsuke staff 96 4 22 10:18 . -rw-r--r-- 1 abe.shinsuke staff 217 4 22 10:18 cdk-test.ts drwxr-xr-x 15 abe.shinsuke staff 480 4 22 10:18 ..
$ cat bin/cdk-test.ts #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from '@aws-cdk/core'; import { CdkTestStack } from '../lib/cdk-test-stack'; const app = new cdk.App(); new CdkTestStack(app, 'CdkTestStack');
そして、 lib
ディレクトリの配下ですが、Construct毎の構成コードが置かれています。
$ ls -lart lib total 8 drwxr-xr-x 3 abe.shinsuke staff 96 4 22 10:18 . -rw-r--r-- 1 abe.shinsuke staff 245 4 22 10:18 cdk-test-stack.ts drwxr-xr-x 15 abe.shinsuke staff 480 4 22 10:18 ..
$ cat lib/cdk-test-stack.ts import * as cdk from '@aws-cdk/core'; export class CdkTestStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here } }
移行のアプローチ
さて、今まで見てきた情報から移行のアプローチを決めます。
- CDKのプロジェクト構成リソースの追加
package.json
の分離- インフラコードの追加とCDKのブートストラップ
- BoltアプリケーションをプロキシするLambdaのハンドラコードを追加
- デプロイしてみる
CDKのプロジェクト構成リソースの追加
ものは試しで今回対象のプロジェクトで cdk init
を実行してみましたが、予想はしていたものの何がしかファイルのある状態での実行はできないようでエラーになりました。
ということで手作業で以下作りました。
ckd.json
bin
ディレクトリとその配下のコード(名前を変えて)lib
ディレクトリとその配下のコード(名前を変えて)
そのほかのファイルについてはとりあえずデプロイを確認するまでであれば不要でしょう。
現段階では何もインフラ構成のコードは書かれていません。
package.json
の分離
次に、実行時に依存するライブラリの分離とLambda Layerとしてデプロイするための準備をします。
まず、 bundle/nodejs
ディレクトリを作成して、カレントディレクトにした後 npm init
を実行して package.json
を作ります。
次に、プロジェクトのルートに戻り、以下二つのパッケージをアンインストールします。
@slack/bolt
dotenv
この時に、一時期にコンパイルエラーになる箇所が発生しますが、次の段階での対処ですぐに解消します。
次に、再度 bundle/nodejs
をカレントディレクトリにして、以下三つのパッケージをインストールします。
@slack/bolt
dotenv
aws-serverless-express
最後に、プロジェクトルートで、 npm install --save-dev bundle/nodejs
を実行してください。
これで、実行時に依存するライブラリとインフラ構成を含めたプロジェクト全体で依存するライブラリの分離は完了です。
インフラコードの追加とCDKのブートストラップ
次に、インフラ構成のコードを書きます。
この段階ではまずシンプルに依存関係を処理するLambda LayerとLambda Functionを作成します。
元ネタに使ったBoltアプリケーションの tsconfig
の設定で、アプリケーションのビルドの結果は dist
以下に出力されるようになっているのでLambda Functionの作成時のアセットは dist
を指定しています。
また、Lambda Layerとして bundle
配下をデプロイします。
Layerとしてデプロイするディレクトリでは事前に npm --prefix /bundle/nodejs install /bundle/nodejs
を実行して node_modules
を作成しておくようにしましょう。
私は、 cdk deploy
コマンドで一気にできるようにプリプロセスとして上記を実行するコードを書きました。
以下、実際のコードの抜粋です。
(インフラ構成のコード)
export class CxJobOfferMeetingToolsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); var region: string; if(props && props.env && props.env.region) { region = props.env.region; } else { region = "ap-northeast-1"; } // Environment variable for lambda function require("dotenv").config(); var lambdaSlackAppEnvironment = { SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET }; // create lambda layer const bundleLayer = new lambda.LayerVersion(this, 'lambdaBundleLayer', { layerVersionName: 'cx-job-offer-meeting-tools-layer', code: new lambda.AssetCode(PreProcess.BUNDLE_LAYER_BASE_DIR), compatibleRuntimes: [lambda.Runtime.NODEJS_10_X], }); const lambdaFunction = new lambda.Function(this, 'jobOfferMeetingSlackApp', { code: lambda.Code.asset('dist/'), handler: `app.handler`, runtime: lambda.Runtime.NODEJS_10_X, timeout: Duration.seconds(3), environment: lambdaSlackAppEnvironment, layers: [bundleLayer], }); } }
(プリプロセス)
import * as child_process from 'child_process'; export class PreProcess { public static BUNDLE_LAYER_BASE_DIR = process.cwd() + "/bundle"; public static BUNDLE_LAYER_RUNTIME_DIR_NAME = "/nodejs" public static generateBundlePackage() { console.log( child_process.execSync( `npm --prefix ${this.getModuleInstallDir()} install ${this.getModuleInstallDir()}`).toString()); } private static getModuleInstallDir(): string { return `${this.BUNDLE_LAYER_BASE_DIR}${this.BUNDLE_LAYER_RUNTIME_DIR_NAME}`; } }
さて、ここまでで一度CDKのブートストラップをしましょう。
Lambda Functionとしてはまだ未完成ですが、少なくともCDKプロジェクトのファイル構成として、致命的な欠落がないかどうかは確認が取れます。 いくつかタイポがあったため、エラーになりましたがそれらを解消した後の実行結果が以下です。
$ cdk bootstrap npx: 8個のパッケージを1.408秒でインストールしました。 npm WARN [email protected] No repository field. audited 188 packages in 1.404s found 0 vulnerabilities ⏳ Bootstrapping environment aws://xxxxxxxxxxxxx/ap-northeast-1... CDKToolkit: creating CloudFormation changeset... 0/2 | 10:28:58 | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StagingBucketPolicy 0/2 | 10:28:59 | CREATE_IN_PROGRESS | AWS::S3::BucketPolicy | StagingBucketPolicy Resource creation Initiated 1/2 | 10:29:00 | CREATE_COMPLETE | AWS::S3::BucketPolicy | StagingBucketPolicy 1/2 | 10:29:02 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | CDKToolkit 2/2 | 10:29:02 | UPDATE_COMPLETE | AWS::CloudFormation::Stack | CDKToolkit ✅ Environment aws://xxxxxxxxxxxxx/ap-northeast-1 bootstrapped.
CDKプロジェクトとしてのブートストラップも完了したようです。
BoltアプリケーションをプロキシするLambdaのハンドラコードを追加
ここはサンプル通りに変更するだけです。
デプロイしてみる
ここまでくればデプロイですが、ブートストラップが成功しているので特になんの問題もなくデプロイできました。
Lambda Layerもデプロイされて、しっかりLambda Functionとも関連づけられています。Lambda Functionとしてデプロイされているコードも、ビルドした dist
配下のものでした。
まとめ
結構大手術になるかな、と予想していたのですが、思っていたよりも
もちろん、アーキテクチャー面で検討しなければならない面は残っていますが、早いうちにデプロイ先を切り替えて整理できる状態に持って行けたのは良いことだと思います。